﻿//
// Copyright (c) 2012 Jeff Wilcox
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

using Microsoft.Phone.Shell;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Resources;
using TheVerge.Base.Models;

//
// This nifty class will help override the runtime resources used in the app's
// instance to a specific theme (Light or Dark), the inversion of the user's
// OS setting, etc.
//
// A static OverrideOptions property can be set *before* calling the theme
// application methods to set choices for altering SystemTray instances, 
// ApplicationBars, etc.
//
// For the best effect, any code inside the application newing up 
// ApplicationBar instances should be replaced by a call to the ThemeManager
// and its CreateApplicationBar instance, or the extension method for 
// theming should be used.
//
// This set of wonderful hacks unfortunately cannot alter the MessageBox that
// the OS displays.
//
// Although this code could be adapted for use with custom app branding, at 
// this time the implementation is not designed for that.
//

namespace Microsoft.Phone.Controls
{
    /// <summary>
    /// Represents a light/dark theme.
    /// </summary>
    public enum ThemeBackground
    {
        Light,
        Dark,
    }

    public class NoColor : AccentColor
    {
        public override Color Color
        {
            get
            {
                if (Theme.Active.Background == ThemeBackground.Light)
                    return Colors.Black;
                else
                    return Colors.White;
            }
            protected set { }
        }
        public NoColor() : base("No Color", 0) { }
    }
    public class AccentColor
    {
        public string Name { get; private set; }
        public virtual Color Color { get; protected set; }
        public Brush Brush { get { return new SolidColorBrush(Color); } }

        public AccentColor(string name, uint val) : this(name, ToColor(val)) { }

        public AccentColor(string name, Color color)
        {
            Name = name;
            Color = color;
        }

        internal static Color ToColor(uint argb)
        {
            return Color.FromArgb((byte)((argb & -16777216) >> 24), (byte)((argb & 0xff0000) >> 16), (byte)((argb & 0xff00) >> 8), (byte)(argb & 0xff));
        }

        internal static uint ColorToUInt(Color color)
        {
            return (uint)((color.A << 24) | (color.R << 16) |
                          (color.G << 8) | (color.B << 0));
        }
    }

    public class AccentColors
    {

        public static IEnumerable<AccentColor> All
        {
            get
            {
                yield return None;
                
                yield return Lime;
                yield return Green;
                yield return Emerald;

                yield return Viridian;
                yield return Blue;
                yield return Cobalt;

                yield return Indigo;
                yield return Pink;
                yield return Magenta;

                yield return Crimson;
                yield return Red;
                yield return Mango;
                yield return Amber;
                yield return Yellow;
                
                yield return Brown;
                yield return Olive;
                yield return Steel;
                yield return Mauve;
                yield return Sienna;
                
                
                yield return Gray;
                yield return NokiaBlue;
                yield return OrangeUK;
                yield return O2Blue;
            }
        }

        //-- See MSDN
        //http://msdn.microsoft.com/en-us/library/windowsphone/develop/ff402557(v=vs.105).aspx

        public static readonly NoColor None = new NoColor();

        public static readonly AccentColor Lime = new AccentColor("Lime", 0xFFA2C139);
        public static readonly AccentColor Green = new AccentColor("Green", 0xFF339933);
        public static readonly AccentColor Emerald = new AccentColor("Emerald", 0xFF008A00);

        public static readonly AccentColor Viridian = new AccentColor("Viridian", 0xFF00ABA9);
        public static readonly AccentColor Blue = new AccentColor("Blue", 0xFF1BA1E2);
        public static readonly AccentColor Cobalt = new AccentColor("Cobalt", 0xFF0050EF);

        public static readonly AccentColor Indigo = new AccentColor("Indigo", 0xFF6A00FF);
        public static readonly AccentColor Pink = new AccentColor("Pink", 0xFFE671B8);
        public static readonly AccentColor Magenta = new AccentColor("Magenta", 0xFFD80073);
        public static readonly AccentColor Crimson = new AccentColor("Crimson", 0xFFA20025);
        public static readonly AccentColor Red = new AccentColor("Red", 0xFFE51400);
        public static readonly AccentColor Mango = new AccentColor("Mango", 0xFFF09609);
        public static readonly AccentColor Amber = new AccentColor("Amber", 0xFFF0A30A);
        public static readonly AccentColor Yellow = new AccentColor("Yellow", 0xFFD8C100);
        

        public static readonly AccentColor Brown = new AccentColor("Brown", 0xFFA05000);
        public static readonly AccentColor Olive = new AccentColor("Olive", 0xFF6D8764);
        public static readonly AccentColor Steel = new AccentColor("Steel", 0xFF647687);

        public static readonly AccentColor Mauve = new AccentColor("Mauve", 0xFF76608A);
        public static readonly AccentColor Sienna = new AccentColor("Sienna", 0xFF7A3B3F);
        

        public static readonly AccentColor Gray = new AccentColor("Gray", 0xFF4B4B4B);
        public static readonly AccentColor NokiaBlue = new AccentColor("Nokia Blue", 0xFF1080DD);
        public static readonly AccentColor OrangeUK = new AccentColor("Orange UK", 0xFFF7610A);
        public static readonly AccentColor O2Blue = new AccentColor("O2 Blue", 0xFF31AFE1);
    }

    /// <summary>
    /// Options for overriding the themes in the application instance with
    /// regards to system components that do not use the XAML static resource
    /// values.
    /// </summary>
    public enum ThemeManagerOverrideOptions
    {
        /// <summary>
        /// No special override options. SystemTray and ApplicationBar 
        /// instances are unaffected and will not be properly themed.
        /// </summary>
        None,

        /// <summary>
        /// Colors system trays appropriately.
        /// </summary>
        SystemTrayColors,

        /// <summary>
        /// Colors system trays appropriately and also any set ApplicationBar
        /// instances. Will not theme ApplicationBar instances that are 
        /// created after the page's Navigated event or that are created and
        /// not set immediately.
        /// </summary>
        SystemTrayAndApplicationBars,
    }

    public class Theme : AVM
    {
        private const string PHONE_THEME = @"PHONE_THEME";
        private const string USER_THEME = @"USER_THEME";
        
        private const string RESOURCE_PhoneAccentBrush = @"PhoneAccentBrush";
        private const string RESOURCE_PhoneDarkThemeVisibility = @"PhoneDarkThemeVisibility";

        public static Theme Active { get; private set; }
        public static Theme Phone { get; private set; }

        static Theme()
        {
            var phoneAccentColor = (Application.Current.Resources[RESOURCE_PhoneAccentBrush] as SolidColorBrush).Color;

            var darkVisibility = (Visibility)Application.Current.Resources[RESOURCE_PhoneDarkThemeVisibility];
            var phoneBackground = darkVisibility == Visibility.Visible ? ThemeBackground.Dark : ThemeBackground.Light;

            Phone = new Theme
            {
                AccentColor = phoneAccentColor,
                Background = phoneBackground,
                SupportedOrientation = SupportedPageOrientation.PortraitOrLandscape,
                UsePhoneTheme = true
            };

            Active = new Theme();

            SetTheme(Phone, PHONE_THEME);
        }

        public Theme()
        {
            FontSizeMinimum = 60D;
            FontSizeMaximum = 200D;
            FontSize = 128D;
            SupportedOrientation = SupportedPageOrientation.PortraitOrLandscape;
        }

        public Color AccentColor { get; set; }
        public ThemeBackground Background { get; set; }
        public SupportedPageOrientation SupportedOrientation
        {
            get { return GetValue(() => SupportedOrientation); }
            set { SetValue(value, () => SupportedOrientation); }
        }
        public bool UsePhoneTheme
        {
            get { return GetValue(() => UsePhoneTheme); }
            set { SetValue(value, () => UsePhoneTheme); }
        }
        
        public double FontSize
        {
            get { return GetValue(() => FontSize); }
            set {
                if (value > FontSizeMaximum || value < FontSizeMinimum) return;
                SetValue(value, () => FontSize); 
            }
        }
        public double FontSizeMinimum { get; private set; }
        public double FontSizeMaximum { get; private set; }

        public Theme Copy()
        {
            var t = new Theme()
            {
                AccentColor = AccentColor,
                Background = Background,
                FontSize = FontSize,
                SupportedOrientation = SupportedOrientation,
                UsePhoneTheme = UsePhoneTheme,
            };
            return t;
        }

        public static void Load() { Load(USER_THEME); }

        public static void SetTheme(Theme t) { SetTheme(t, USER_THEME); }

        #region Helpers
        private static void ApplyActive()
        {
            ThemeManager.SetAccentColor(Active.AccentColor);

            if (Active.Background == ThemeBackground.Light)
                ThemeManager.ToLightTheme();
            else
                ThemeManager.ToDarkTheme();
        }

        private static void Load(string key)
        {
            if (IsolatedStorageSettings.ApplicationSettings.Contains(key))
                SetTheme(IsolatedStorageSettings.ApplicationSettings[key] as Theme);
        }

        private static void SaveTheme(Theme t, string key)
        {
            IsolatedStorageSettings.ApplicationSettings[key] = t;
        }

        private static void SetTheme(Theme t, string key)
        {
            if (t.UsePhoneTheme)
            {
                Active.AccentColor = Phone.AccentColor;
                Active.Background = Phone.Background;
            }
            else
            {
                Active.AccentColor = t.AccentColor;
                Active.Background = t.Background;
            }
            
            
            Active.FontSize = t.FontSize;
            Active.SupportedOrientation = t.SupportedOrientation;
            Active.UsePhoneTheme = t.UsePhoneTheme;

            ApplyActive();

            SaveTheme(t, key);
        }
        #endregion
    }

    /// <summary>
    /// A specialized Windows Phone theme management class that can be used to
    /// override the user's selected Light/Dark operating system for the 
    /// running app instance. It must be overridden inside the App::App() 
    /// constructor (or early in initialization) and cannot be changed at 
    /// runtime or called more than once.
    /// </summary>
    public static class ThemeManager
    {
        static ThemeManager()
        {
            // The default system tray overriding value. This will have a 
            // slight performance impact on all navigations, etc., hence the
            // ability to turn off. It must be set to False before calling any
            // theme override methods, however - there's no property change
            // notifications on the value.
            OverrideOptions = ThemeManagerOverrideOptions.SystemTrayAndApplicationBars;

            Accent = (Color)Application.Current.Resources["PhoneAccentColor"];
            Background = (Color)Application.Current.Resources["PhoneBackgroundColor"];
            Chrome = (Color)Application.Current.Resources["PhoneChromeColor"];
            Foreground = (Color)Application.Current.Resources["PhoneForegroundColor"];
            Subtle = (Color)Application.Current.Resources["PhoneSubtleColor"];
        }

        /// <summary>
        /// Gets or sets a value indicating whether to hook up to navigation
        /// events to update system tray background and foreground colors with
        /// the overriden values.
        /// </summary>
        public static ThemeManagerOverrideOptions OverrideOptions { get; set; }

        public static Color Accent { get; private set; }
        public static Color Background { get; private set; }
        public static Color Chrome { get; private set; }
        public static Color Foreground { get; private set; }
        public static Color Subtle { get; private set; }
        public static ThemeBackground Theme { get; private set; }

        /// <summary>
        /// An extension method for ApplicationBars, will apply the overridden
        /// theme's coloring to the app bar if needed.
        /// </summary>
        /// <param name="bar">The application bar instance.</param>
        public static void MatchOverriddenTheme(this IApplicationBar bar)
        {
            if (bar != null) // Only if actually override the theme.
            {
                bar.BackgroundColor = Chrome;

                // This probably isn't the exact resource to reference but it 
                // looks correct.
                bar.ForegroundColor = Foreground;
            }
        }

        /// <summary>
        /// Creates a new instance of ApplicationBar and applies any 
        /// overridden theme coloring to the instance.
        /// </summary>
        /// <returns>Returns a new ApplicationBar instance with its foreground
        /// and background colors set.</returns>
        public static ApplicationBar CreateApplicationBar()
        {
            var ab = new ApplicationBar();
            ab.MatchOverriddenTheme();
            return ab;
        }

        /// <summary>
        /// Overrides the theme resources used at runtime for this instance.
        /// Will not make any changes to the style resources if the requested
        /// theme is the same as the runtime theme at startup.
        /// </summary>
        /// <param name="theme">The preferred Theme to override to.</param>
        public static void OverrideTheme(ThemeBackground theme)
        {
            if (IsThemeAlready(theme))
            {
                Debug.WriteLine("The user's theme is already set to the " + theme + " theme. No resources are being overwritten.");
            }
            else
            {
                Debug.WriteLine("Overriding resources to match the " + theme + " theme.");

                new RuntimeThemeResources().Apply(theme);
            }
        }

        private static bool IsThemeAlready(ThemeBackground theme)
        {
            double current = (double)Application.Current.Resources["PhoneDarkThemeOpacity"];
            double requested = theme == ThemeBackground.Dark ? 1.0 : 0.0;

            // Double comparisons can be scary but these are straightforward.
            return current == requested;
        }

        /// <summary>
        /// Overrides the theme resources used at runtime to be the inverse of
        /// the user's current operating system theme setting.
        /// </summary>
        public static void InvertTheme()
        {
            OverrideTheme(IsThemeAlready(ThemeBackground.Dark) ? ThemeBackground.Light : ThemeBackground.Dark);
        }

        /// <summary>
        /// Overrides the theme resources used at runtime to be the Light 
        /// theme.
        /// </summary>
        public static void ToLightTheme()
        {
            OverrideTheme(ThemeBackground.Light);
        }

        /// <summary>
        /// Overrides the theme resources used at runtime to be the Dark theme.
        /// </summary>
        public static void ToDarkTheme()
        {
            OverrideTheme(ThemeBackground.Dark);
        }

        /// <summary>
        /// Overrides the accent color and brush used at runtime to a new one.
        /// </summary>
        /// <param name="color">A Color to set the accent brush/color to.</param>
        public static void SetAccentColor(Color color)
        {
            Accent = color;
            RuntimeThemeResources.DualColorValue.SetColorAndBrush("PhoneAccent", color);
        }

        /// <summary>
        /// A specialized private class for handling dark/light theme resource
        /// overrides.
        /// </summary>
        private class RuntimeThemeResources
        {
            private List<ThemeValue> _values;

            private static readonly DualColorValue _Background = new DualColorValue(0xFF000000, 0xFFFFFFFF);
            private static readonly DualColorValue _Foreground = new DualColorValue(0xFFFFFFFF, 0xDE000000);
            private static readonly DualColorValue _Chrome = new DualColorValue(0xFF1F1F1F, 0xFFDDDDDD);
            private static readonly DualColorValue _Subtle = new DualColorValue(0x99FFFFFF, 0x66000000);

            public RuntimeThemeResources()
            {
                // These initial constants come from the Windows Phone design
                // resources: C:\Program Files (x86)\Microsoft SDKs\Windows Phone\v7.1\Design\
                _values = new List<ThemeValue>
                {
                    // The prefix value actually is the text after Phone.
                    // For colors and brushes (it's assumed both exist), no
                    // suffix is in the name.

                    new ThemeValue("Background", _Background),
                    new ThemeValue("Foreground", _Foreground),
                    new ThemeValue("Chrome", _Chrome),

                    new ThemeValue("ContrastForeground", new DualColorValue(0xFF000000, 0xFFFFFFFF)),
                    new ThemeValue("ContrastBackground", new DualColorValue(0xFFFFFFFF, 0xDE000000)),
                    new ThemeValue("Disabled", new DualColorValue(0x66FFFFFF, 0x4D000000)),
                    new ThemeValue("Subtle", _Subtle),

                    new ThemeValue("TextCaret", new DualColorValue(0xFF000000, 0xDE000000)),
                    new ThemeValue("TextBox", new DualColorValue(0xBFFFFFFF, 0x26000000)),
                    new ThemeValue("TextBoxForeground", new DualColorValue(0xFF000000, 0xDE000000)),
                    new ThemeValue("TextBoxEditBackground", new DualColorValue(0xFFFFFFFF, 0x00000000)),
                    new ThemeValue("TextBoxEditBorder", new DualColorValue(0xFFFFFFFF, 0xDE000000)),
                    new ThemeValue("TextBoxReadOnly", new DualColorValue(0x77000000, 0x2E000000)),

                    new ThemeValue("RadioCheckBox", new DualColorValue(0xBFFFFFFF, 0x26000000)),
                    new ThemeValue("RadioCheckBoxDisabled", new DualColorValue(0x66FFFFFF, 0x00000000)),
                    new ThemeValue("RadioCheckBoxCheck", new DualColorValue(0xFF000000, 0xDE000000)),
                    new ThemeValue("RadioCheckBoxCheckDisabled", new DualColorValue(0x66000000, 0x4D000000)),
                    new ThemeValue("RadioCheckBoxPressed", new DualColorValue(0xFFFFFFFF, 0x00000000)),
                    new ThemeValue("RadioCheckBoxPressedBorder", new DualColorValue(0xFFFFFFFF, 0xDE000000)),

                    new ThemeValue("Semitransparent", new DualColorValue(0xAA000000, 0xAAFFFFFF)),

                    new ThemeValue("Inactive", new DualColorValue(0x33FFFFFF, 0x33000000)),
                    new ThemeValue("InverseInactive", new DualColorValue(0xFFCCCCCC, 0xFFE5E5E5)),
                    new ThemeValue("InverseBackground", new DualColorValue(0xFFFFFFFF, 0xFFDDDDDD)),

                    new ThemeValue("Border", new DualColorValue(0xBFFFFFFF, 0x99000000)),

                    new ThemeValue("DarkThemeVisibility", new DualValue<Visibility>(Visibility.Visible, Visibility.Collapsed)),
                    new ThemeValue("LightThemeVisibility", new DualValue<Visibility>(Visibility.Collapsed, Visibility.Visible)),

                    new ThemeValue("DarkThemeOpacity", new DualValue<double>(1.0, 0.0)),
                    new ThemeValue("LightThemeOpacity", new DualValue<double>(0.0, 1.0)),
                };

                // Could customize this class to offer debug/testing-specific
                // values or branding themes here.
            }

            internal class DualColorValue : IDualValue
            {
                private uint _dark;
                private uint _light;

                public DualColorValue(uint dark, uint light)
                {
                    _dark = dark;
                    _light = light;
                }

                public DualColorValue(Color dark, Color light)
                {
                    _dark = AccentColor.ColorToUInt(dark);
                    _light = AccentColor.ColorToUInt(light);
                }

                internal static void SetColorAndBrush(string prefix, Color color)
                {
                    var currentColor = new Color();
                    // Check if the Colour is actually in the dictionary
                    if (Application.Current.Resources.Contains(prefix + "Color"))
                    {
                        currentColor = (Color)Application.Current.Resources[prefix + "Color"];
                        currentColor.A = color.A;
                        currentColor.B = color.B;
                        currentColor.G = color.G;
                        currentColor.R = color.R;
                    }
                    else
                    {
                        // If it's not in the dictionary, add it.
                        currentColor = color;
                        Application.Current.Resources.Add(prefix + "Color", currentColor);
                    }

                    // Check if the Brush is actually in the dictionary
                    if (Application.Current.Resources.Contains(prefix + "Brush"))
                    {
                        var brush = (SolidColorBrush)Application.Current.Resources[prefix + "Brush"];
                        brush.Color = currentColor;
                    }
                    else
                    {
                        // If it's not in the dictionary, add it.
                        var brush = new SolidColorBrush
                                        {
                                            Color = currentColor
                                        };
                        Application.Current.Resources.Add(prefix + "Brush", brush);
                    }
                }

                public object Value(ThemeBackground theme)
                {
                    return AccentColor.ToColor(theme == ThemeBackground.Dark ? _dark : _light);
                }

                public void Apply(ThemeBackground theme, string prefix)
                {
                    string name = "Phone" + prefix;
                    var value = (Color)(Value(theme));
                    SetColorAndBrush(name, value);
                }
            }

            private class DualValue<T> : IDualValue
            {
                protected T _dark;
                protected T _light;

                public DualValue(T dark, T light)
                {
                    _dark = dark;
                    _light = light;
                }

                public object Value(ThemeBackground theme)
                {
                    return theme == ThemeBackground.Dark ? _dark : _light;
                }

                public void Apply(ThemeBackground theme, string prefix)
                {
                    string name = "Phone" + prefix;
                    Application.Current.Resources.Remove(name);
                    Application.Current.Resources.Add(name, Value(theme));
                }
            }

            private interface IDualValue
            {
                object Value(ThemeBackground theme);
                void Apply(ThemeBackground theme, string prefix);
            }

            public void Apply(ThemeBackground theme)
            {
                foreach (var value in _values)
                {
                    value.Apply(theme);
                }

                // Using navigation events, hook up to alter the system tray
                // values as well as the page's set application bar.
                AttachRootFrameNavigationEvents(theme);// (Color)_Background.Value(theme), (Color)_Foreground.Value(theme), (Color)_Chrome.Value(theme));
                // We pass the memory savings on to you!
                _values = null;
            }

            private void AttachOnRootFrameReady(PhoneApplicationFrame frame)
            {
                frame.Navigated += (x, xe) =>
                {
                    var page = xe.Content as PhoneApplicationPage;
                    if (page != null)
                    {
                        SetSystemComponentColors(page, ThemeManager.Background, ThemeManager.Foreground, ThemeManager.Chrome);
                    }
                };

                var firstPage = frame.Content as PhoneApplicationPage;
                SetSystemComponentColors(firstPage, ThemeManager.Background, ThemeManager.Foreground, ThemeManager.Chrome);
            }

            private void SetSystemComponentColors(PhoneApplicationPage page, Color background, Color foreground, Color chrome)
            {
                if (page != null)
                {
                    SystemTray.SetBackgroundColor(page, background);
                    SystemTray.SetForegroundColor(page, foreground);

                    if (OverrideOptions == ThemeManagerOverrideOptions.SystemTrayAndApplicationBars)
                    {
                        var appBar = page.ApplicationBar as IApplicationBar;
                        if (appBar != null)
                        {
                            appBar.MatchOverriddenTheme();
                        }
                    }
                }
            }

            private void AttachRootFrameNavigationEvents(ThemeBackground theme)
            {
                ThemeManager.Theme = theme;

                ThemeManager.Background = (Color)_Background.Value(theme);
                ThemeManager.Chrome = (Color)_Chrome.Value(theme);
                ThemeManager.Foreground = (Color)_Foreground.Value(theme);
                ThemeManager.Subtle = (Color)_Subtle.Value(theme);

                // Loop until the root frame is set for real. To really 
                // improve startup performance, instead the system could take 
                // in a reference to the Frame object and we could require the
                // caller of theme functions to instead use that.
                var frame = Application.Current.RootVisual;
                if (frame != null)
                {
                    if (!(frame is Canvas))
                    {
                        // We have the real frame now. Unfortunately this may
                        // increase the fill count heavily in the app by += 1.
                        var asControl = frame as Control;
                        if (asControl != null)
                        {
                            asControl.Background = new SolidColorBrush(ThemeManager.Background);
                        }

                        // Hook up to the navigation events for the tray.
                        if (OverrideOptions == ThemeManagerOverrideOptions.SystemTrayAndApplicationBars ||
                            OverrideOptions == ThemeManagerOverrideOptions.SystemTrayColors)
                        {
                            PhoneApplicationFrame paf = frame as PhoneApplicationFrame;
                            if (paf != null)
                            {
                                AttachOnRootFrameReady(paf);
                            }
                        }

                        return;
                    }
                }

                Deployment.Current.Dispatcher.BeginInvoke(() => AttachRootFrameNavigationEvents(theme));
            }

            private class ThemeValue
            {
                public ThemeValue(string prefix, IDualValue val)
                {
                    _prefix = prefix;
                    _value = val;
                }

                public string _prefix;
                public IDualValue _value;

                public void Apply(ThemeBackground theme)
                {
                    _value.Apply(theme, _prefix);
                }
            }
        }
    }
}
